classdef dotsTheDrawablesManager < dotsAllRemoteObjectManagers
    % @class dotsTheDrawablesManager
    % Coordinate dotsDrawable* objects with dotsTheScreen.
    % @details
    % dotsTheDrawablesManager creates and organizes instances of
    % dotsDrawable* classes.  It's also a front-end for working with
    % dotsTheScreen.  For dotsDrawable* instances and for dotsTheScreen,
    % dotsTheDrawablesManager knows how to corrdinate behavior between
    % client and server instances of Snow Dots, including across a network.
    % @details
    % Any Snow Dots program that might need to run with a remote graphics
    % server should use dotsTheDrawables manager instead of working with
    % dotsDrawable* instances dotsTheScreen directly.  This includes
    % dotsDrawable* creation and other method calls.  It also includes
    % opening and closing drawing windows.
    % @details
    % The exception is setting properties for dotsDrawable* instances.
    % Users can set dotsDrawable* properties with normal Matlab syntax
    % because instances will report property changes to
    % dotsTheDrawablesManager automatically.
    %
    % @ingroup drawable
    
    properties
        % true or false, whether to clear old graphics after each frame
        % @details
        % Use the setIsAccumulating() method instead of normal property
        % setting, for two reasons:
        %   - allows correct behavior for the server counterpart
        %   - easier to set with an anonymous function or fevalable
        % .
        isAccumulating;
        
        % true or false, whether to draw new frames during runBriefly().
        % @details
        % Users should pass an isAnimating argument to mayDrawNextFrame(),
        % rather than setting isAnimating directly.
        isAnimating;
        
        % string name of managedObjects group to draw as a batch
        % @details
        % Use the activateGroup() method instead of normal property
        % setting, for two reasons:
        %   - allows correct behavior for the server counterpart
        %   - easier to set with an anonymous function or fevalable
        % .
        activeGroup;
        
        % cell array of dotsDrawable objects corresponding to activeGroup
        activeDrawables;
        
        % the dotsTheScreen singleton instance
        theScreen;
        
        % topsGroupedList with information about dropped graphics frames.
        % @details
        % droppedFrameList stores the onset times for graphics frames that
        % were late.  "Late" means that at least two video refresh cycles
        % passed between calls to mayDrawNextFrame().  This may only matter
        % during smooth animation.
        % @details
        % Each group in the list is the name of the activeGroup at the
        % time.  Each mnemonic in the list is the onset time of a "late"
        % frame.
        droppedFrameList;
        
        % struct of timing information from the last mayDrawNextFrame()
        lastFrameInfo;
        
        % pixel dimensions of the OpenGL drawing window [x y x2 y2]
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowRect;
        
        % Hz frame rate of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowFrameRate;
        
        % a foreground color, [L] [LA] [RGB] [RGBA], 0-255
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        foregroundColor;
        
        % a background color, [L] [LA] [RGB] [RGBA], 0-255
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        backgroundColor;
    end
    
    properties (Hidden)
        % value returned from a getScreenProperty transacton
        screenProperty;
    end
    
    methods (Access=private)
        % Constructor is private.
        % @details
        % Use dotsTheDrawablesManager.theObject to access the current
        % instance.
        function self = dotsTheDrawablesManager(varargin)
            dotsTheMachineConfiguration.applySettingsToObject(self, true);
            self.set(varargin{:});
            self.initialize;
        end
    end
    
    methods (Static=true)
        % Access the current instance.
        function obj = theObject(varargin)
            persistent self
            if isempty(self) || ~isvalid(self)
                constructor = str2func(mfilename);
                self = feval(constructor, varargin{:});
            else
                self.set(varargin{:});
            end
            obj = self;
        end
        
        % Restore the current instance to a fresh state.
        function reset(varargin)
            factory = str2func([mfilename, '.theObject']);
            self = feval(factory, varargin{:});
            self.initialize;
        end
        
        % Launch a graphical interface to view managed dotsDrawable
        % objects.
        function g = gui
            self = dotsTheDrawablesManager.theObject;
            g = self.managedObjects.gui;
        end
    end
    
    methods
        % Restore the manager singleton to a fresh state.
        function initialize(self)
            self.isAnimating = false;
            
            self.initialize@dotsAllRemoteObjectManagers;
            
            self.mayRequestTransaction = false;
            
            self.initializeLists({'droppedFrameList'});
            
            % get shared property values from the screen
            dotsTheScreen.reset();
            self.theScreen = dotsTheScreen.theObject;
            dotsTheSwitchboard.shareManyPropertiesOfObject( ...
                self.theScreen.sharedProperties, self);
            
            % get more shared property values
            dotsTheSwitchboard.sharePropertyOfObject( ...
                'foregroundColor', self);
            dotsTheSwitchboard.sharePropertyOfObject( ...
                'backgroundColor', self);
            
            self.setIsAccumulating(false);
            self.lastFrameInfo = [];
            self.activateGroup('none');
            
            self.mayRequestTransaction = true;
        end
        
        % Extend dotsAllRemoteObjectManagers to also reset dotsTheScreen
        function softReset(self)
            self.softReset@dotsAllRemoteObjectManagers;
            dotsTheScreen.reset();
        end
        % Carry out one transaction and draw one frame, as needed.
        % @details
        % Redefines the runBriefly() method of dotsAllRemoteObjectManagers
        % to also draw a frame of animation, if isAnimating is true.
        function runBriefly(self)
            if self.isAnimating && (self.serverMode || ~self.clientMode)
                self.drawNextFrame;
            end
            self.runBriefly@dotsAllRemoteObjectManagers;
        end
        
        function setIsAccumulating(self, isAccumulating)
            self.isAccumulating = isAccumulating;
            if self.clientMode && self.mayRequestTransaction;
                self.setObjectProperty( ...
                    self, 'isAccumulating', isAccumulating);
            end
        end
        
        % Prepare to draw a group of drawables as a batch.
        % @param group string name of a managedObjects group
        % @details
        % Sets activeGroup for the client and server managers.  The objects
        % in the named @a group will be copied to activeDrawables.  If
        % windowNumber is a valid Screen() drawing window, each object in
        % @a group will have its prepareToDrawInWindow() invoked.
        % @details
        % Subsequent calls to mayDrawNextFrame() will invoke mayDrawNow()
        % for each @a group object.
        function activateGroup(self, group)
            self.activeGroup = group;
            if self.clientMode && self.mayRequestTransaction;
                self.setObjectProperty( ...
                    self, 'activeGroup', group);
            end
        end
        
        % Account for an activated group.
        % @details
        % See activateGroup().
        function set.activeGroup(self, activeGroup)
            self.activeGroup = activeGroup;
            if self.managedObjects.containsGroup(activeGroup)
                self.activeDrawables = self.getGroup(activeGroup);
                self.prepareActiveGroupForWindow;
            else
                self.activeDrawables = {};
            end
        end
        
        % Open an OpenGL drawing window via dotsTheScreen.
        function openScreenWindow(self)
            if self.clientMode && self.mayRequestTransaction;
                % request a remote window and get info (synchronously)
                self.requestTransaction( ...
                    [], [], false, 'openScreenWindow', {});
                self.updateScreenSharedProperties;
                
            else
                % open a real window
                self.theScreen.open;
                self.updateScreenSharedProperties;
                self.prepareActiveGroupForWindow;
            end
        end
        
        % Close the OpenGL drawing window via dotsTheScreen.
        function closeScreenWindow(self)
            self.isAnimating = false;
            
            if self.clientMode && self.mayRequestTransaction
                % close a remote window and get info (synchronously)
                self.requestTransaction( ...
                    [], [], false, 'closeScreenWindow', {});
                
            else
                % close the window
                self.theScreen.close;
            end
            
            self.updateScreenSharedProperties;
        end
        
        % Invoke prepareToDrawInWindow() for objects in activeGroup.
        function prepareActiveGroupForWindow(self)
            if self.theScreen.getDisplayNumber >= 0
                drawList = self.activeDrawables;
                for ii = 1:length(drawList)
                    drawList{ii}.prepareToDrawInWindow;
                end
            end
        end
        
        % Draw a frame for the drawables in activeDrawables.
        % @param isAnimating optional true or false, whether to continue
        % drawing frames during subsequent calls to runBriefly().
        % %details
        % Invokes mayDrawNow() for each of the drawables activeDrawables,
        % then invokes dotsTheScreen.nextFrame() to show the results of
        % drawing.  If dotsTheScreen determines that the this frame and the
        % previous frame were not back-to-back, adds an entry to
        % droppedFrameList.
        % @details
        % Returns a struct of timing information from nextFrame(), and
        % assigns the value to lastFrameInfo.  The struct has the folowing
        % fields:
        %   - @b onsetTime estimated onset time for this frame, which might
        %   be a time in the future
        %   - @b onsetFrame number of frames elapsed between
        %   openScreenWindow() and this frame
        %   - @b swapTime estimated time of the last video hardware refresh
        %   (e.g. "vertical blank"), which is alwasy a time in the past
        %   - @b isTight whether this frame and the previous frame were
        %   adjacent (false if a frame was dropped)
        %   .
        %
        function lastFrameInfo = mayDrawNextFrame(self, isAnimating)
            if nargin > 1
                self.isAnimating = isAnimating;
                args = {isAnimating};
            else
                args = {};
            end
            
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction([], 'lastFrameInfo', true, ...
                    'mayDrawNextFrame', args);
            else
                self.lastFrameInfo = self.drawNextFrame();
            end
            
            lastFrameInfo = self.lastFrameInfo;
            if isstruct(lastFrameInfo) ...
                    && ~isnan(lastFrameInfo.onsetTime) ...
                    && ~lastFrameInfo.isTight
                
                self.droppedFrameList.addItemToGroupWithMnemonic( ...
                    lastFrameInfo.onsetTime, ...
                    self.activeGroup, ...
                    lastFrameInfo.onsetTime);
            end
        end
        
        % Get the number of the display used for drawing.
        % @details
        % If Snow Dots has an open drawing window, returns a non-negative
        % integer which corresponds to dotsTheScreen.displayIndex.
        % Otherwise, returns -1.
        % @details
        % See dotsTheScreen.getDisplayNumber().
        function displayNumber = getScreenDisplayNumber(self)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction([], 'screenProperty', true, ...
                    'getScreenDisplayNumber', {});
                displayNumber = self.screenProperty;
            else
                displayNumber = self.theScreen.getDisplayNumber();
            end
        end
        
        % Get a property of dotsTheScreen, locally or remotely.
        % @param property one of the property names of dotsTheScreen
        % @details
        % Accesses the value of one of the properties of dotsTheScreen.  If
        % necessary, accesses the dotsTheScreen used by the server
        % counterpart of dotsTheDrawablesManager.
        % @details
        % Returns the value of @a property for dotsTheScreen.
        function value = getScreenProperty(self, property)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction([], 'screenProperty', true, ...
                    'getScreenProperty', {property});
                value = self.screenProperty;
            else
                value = self.theScreen.(property);
            end
        end
        
        % Set a property of dotsTheScreen, locally or remotely.
        % @param property one of the property names of dotsTheScreen
        % @param value the value to set to @a property
        % @details
        % Modifies the value of one of the properties of dotsTheScreen.  If
        % necessary, modifies the dotsTheScreen used by the server
        % counterpart of dotsTheDrawablesManager.
        % @details
        % Returns the value of @a property for dotsTheScreen that was set.
        function value = setScreenProperty(self, property, value)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction([], [], false, ...
                    'setScreenProperty', {property, value});
                value = self.screenProperty;
            else
                self.theScreen.(property) = value;
            end
        end
        
        % Convert a size from degrees visual angle to pixels.
        % @param degrees a size to convert to pixels
        % @details
        % Converts the given graphical size from degrees of visual angle,
        % to pixels, based on dotsTheScreen.pixelsPerDegree.  The
        % conversion is only approximate.
        function pixels = degreeSizeToPixels(self, degrees)
            pixelsPerDegree = self.getScreenProperty('pixelsPerDegree');
            pixels = degrees.*pixelsPerDegree;
        end
        
        % Convert a size from pixels to degrees visual angle.
        % @param pixels a size to convert to degrees
        % @details
        % Converts the given graphical size from pixels to degrees of
        % visual angle, based on dotsTheScreen.pixelsPerDegree.  The
        % conversion is only approximate.
        function degrees = pixelSizeToDegrees(self, pixels)
            pixelsPerDegree = self.getScreenProperty('pixelsPerDegree');
            degrees = pixels./pixelsPerDegree;
        end
        
        % Convert a window location from degrees visual angle to pixels.
        % @param degreePoint [x y] location to convert to pixels
        % @details
        % Converts the given screen location from degrees of visual angle
        % to pixels, based on dotsTheScreen.pixelsPerDegree and coordinate
        % conventions.  The conversion is only approximate.
        % @details
        % Assumes a planar pixel coordinate system with the origin at the
        % upper left corner of the window, increasing x moving rightward,
        % and increasing y moving downward.
        % @details
        % Approximates degrees of visual angle as a planar coordinate
        % system with the origin at the center of the window, increasing x
        % moving rightward, and increasing y moving upward.  Degrees of
        % visual angle should be a spherical coordinate system, but this
        % would be cumbersome with OpenGL.
        function pixelPoint = degreePointToPixels(self, degreePoint)
            pixelsPerDegree = self.getScreenProperty('pixelsPerDegree');
            self.windowRect = self.getScreenProperty('windowRect');
            wPix = self.windowRect(3) - self.windowRect(1);
            hPix = self.windowRect(4) - self.windowRect(2);
            pixelPoint(1) = wPix/2 + degreePoint(1)*pixelsPerDegree;
            pixelPoint(2) = hPix/2 - degreePoint(2)*pixelsPerDegree;
        end
        
        % Convert a window location from pixels to degrees visual angle.
        % @param point [x y] location to convert to degrees
        % @details
        % Converts the given screen location from pixels to degrees of
        % visual angle, based on dotsTheScreen.pixelsPerDegree and
        % coordinate conventions.  The conversion is only approximate.
        % @details
        % Assumes a planar pixel coordinate system with the origin at the
        % upper left corner of the window, increasing x moving rightward,
        % and increasing y moving downward.
        % @details
        % Approximates degrees of visual angle as a planar coordinate
        % system with the origin at the center of the window, increasing x
        % moving rightward, and increasing y moving upward.  Degrees of
        % visual angle should be a spherical coordinate system, but this
        % would be cumbersome with OpenGL.
        function degreePoint = pixelPointToDegrees(self, pixelPoint)
            pixelsPerDegree = self.getScreenProperty('pixelsPerDegree');
            self.windowRect = self.getScreenProperty('windowRect');
            wPix = self.windowRect(3) - self.windowRect(1);
            hPix = self.windowRect(4) - self.windowRect(2);
            degreePoint(1) = (pixelPoint(1) - wPix/2)/pixelsPerDegree;
            degreePoint(2) = (hPix/2 - pixelPoint(2))/pixelsPerDegree;
        end
    end
    
    methods (Access = protected)
        % Draw a frame of graphics (used internally).
        function frameInfo = drawNextFrame(self)
            drawList = self.activeDrawables;
            for ii = 1:length(drawList)
                drawList{ii}.mayDrawNow;
            end
            
            [onsetTime, onsetFrame, swapTime, isTight] = ...
                self.theScreen.nextFrame(~self.isAccumulating);
            frameInfo = struct( ...
                'onsetTime', {onsetTime}, ...
                'onsetFrame', {onsetFrame}, ...
                'swapTime', {swapTime}, ...
                'isTight', {isTight});
        end
        
        % Update shared properties of dotsTheScreen (used internally).
        function updateScreenSharedProperties(self)
            shared = self.theScreen.sharedProperties;
            for ii = 1:length(shared)
                property = shared{ii};
                value = self.getScreenProperty(property);
                dotsTheSwitchboard.setSharedPropertyValue(property, value);
            end
        end
    end
end